package rdbms;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedHashMap;

import rdbms.ddl.Database;
import rdbms.ddl.Field;
import rdbms.ddl.Table;
import valuetypes.DBValue;
import valuetypes.NumericValue;
import exception.DBValueException;
import exception.EntryException;
import exception.IllegalNameException;
import exception.InvalidFieldException;
import exception.NoSuchDatabaseException;


public class RDBMS {
	
	private static final String TAG_PREFIX = "#";
	private static final int READ_MODE_DATABASE = 1;
	private static final int READ_MODE_TABLE = 2;
	private static final int READ_MODE_FIELD = 3;
	private static final int READ_MODE_GROUP = 4;
	private static final int READ_MODE_GROUP_COLUMN = 5;
	private static final int READ_MODE_ENTRY = 6;
	private static final String FILE_TAG_GROUP_COLUMN = TAG_PREFIX + "groupcolumns";
	private static final String FILE_TAG_DB = TAG_PREFIX + "databases";
	private static final String FILE_TAG_TABLE = TAG_PREFIX + "tables";
	private static final String FILE_TAG_FIELD = TAG_PREFIX + "fields";
	private static final String FILE_TAG_GROUP = TAG_PREFIX + "groups";
	private static final String FILE_TAG_ENTRY = TAG_PREFIX + "entries";
	private LinkedHashMap<String, Integer> databasesIndexName;
	private LinkedHashMap<Integer, Database> databases;
	private int dbIdGen = 0;
	
	
	public RDBMS(){
		databasesIndexName = new LinkedHashMap<String, Integer>();
		databases = new LinkedHashMap<>();
	}
	
	public Database createDatabase(String databaseName) throws IllegalNameException{
		if(databaseName == null){
			throw IllegalNameException.nameCannotBeNull();
		}
		else if(databasesIndexName.containsKey(databaseName)){
			throw IllegalNameException.databaseExists(databaseName);
		}
		else{
			int id = ++dbIdGen;
			Database db = new Database(new NumericValue(id), databaseName);
			addDatabase(db);
			return db;
		}		
	}
	
	public int getDatabaseID(String dbName){
		return databasesIndexName.get(dbName);
	}
	
	public Database deleteDatabase(String dbName){
		return databases.remove(getDatabaseID(dbName));
	}
	
	public Database getDatabase(String dbName){
		return databases.get(getDatabaseID(dbName));
	}
	
	public static RDBMS importFromFile(String filename) throws IOException, DBValueException, NoSuchDatabaseException, IllegalNameException, InvalidFieldException, EntryException{
		FileInputStream fis = new FileInputStream(filename);
		BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
		
		int mode = 0;//undefined
		ArrayList<byte[]> dbData = new ArrayList<>();
		ArrayList<byte[]> tableData = new ArrayList<>();
		ArrayList<byte[]> fieldData = new ArrayList<>();
		ArrayList<byte[]> groupData = new ArrayList<>();
		ArrayList<byte[]> groupcolumnData = new ArrayList<>();
		ArrayList<byte[]> entryData = new ArrayList<>();

		String s;
		while((s = reader.readLine()) != null){
			if(s.startsWith(TAG_PREFIX)){
				if(s.equals(FILE_TAG_DB)){
					mode = READ_MODE_DATABASE;
				}			
				else if(s.equals(FILE_TAG_TABLE)){
					mode = READ_MODE_TABLE;//tables
				}
				else if(s.equals(FILE_TAG_FIELD)){
					mode = READ_MODE_FIELD;
				}
				else if(s.equals(FILE_TAG_GROUP)){
					mode = READ_MODE_GROUP;
				}
				else if(s.equals(FILE_TAG_GROUP_COLUMN)){
					mode = READ_MODE_GROUP_COLUMN;
				}
				else if(s.equals(FILE_TAG_ENTRY)){
					mode = READ_MODE_ENTRY;
				}
				else mode = 0;//bad mode, ignore!
			}			
			else if(s.trim().length() > 0){
				byte[] data = s.getBytes();				
				switch(mode){ 
					case READ_MODE_DATABASE: 						
						dbData.add(data);						
						break;
					case READ_MODE_TABLE:
						tableData.add(s.getBytes());
						System.out.println("Table: " + s);
						break;
					case READ_MODE_FIELD:
						fieldData.add(s.getBytes());
						System.out.println("Field: " + s);
						break;
					case READ_MODE_GROUP:
						groupData.add(s.getBytes());
						System.out.println("Group: " + s);
						break;
					case READ_MODE_GROUP_COLUMN:
						groupcolumnData.add(s.getBytes());
						System.out.println("GroupColumn: " + s);
						break;
					case READ_MODE_ENTRY:
						entryData.add(s.getBytes());
						System.out.println("Entry: " + s);
						break;
					default:
						break;
				}
			}
		}
		reader.close();
		fis.close();
		
		RDBMS rdbms = new RDBMS();
		for(byte[] data : dbData){
			Database db = new Database(data);			
			rdbms.addDatabase(db);
			System.out.println("Database: " + db.toString());
		}		
		for(byte[] data : tableData){
			Table table = new Table(data, rdbms);			
			System.out.println("Table: " + table.toString());
		}		
		for(byte[] data : fieldData){
			Field f = Field.inflateFieldData(data, rdbms);
			System.out.println("Field: " + f);
		}		
		Group.readAllFromData(groupData, groupcolumnData, rdbms, true);
		
		for(byte[] data: entryData){
			Table.insertData(data, rdbms, true);
		}		
		return rdbms;
	}
	
	private void addDatabase(Database db) {
		int dbId = db.getId().getValue();
		if(db.getId().getValue() > dbIdGen){
			dbIdGen = dbId;
		}
		databases.put(dbId, db);
		databasesIndexName.put(db.getName(), dbId);
	}

	public void export(String filename) throws IOException, DBValueException{
		FileOutputStream fos = new FileOutputStream(filename);
		byte[] endl = "\n".getBytes();
		ArrayList<Table> tables = new ArrayList<>();
		ArrayList<Group> groups = new ArrayList<>();
		ArrayList<Field> fields = new ArrayList<>();
		ArrayList<Entry> entries = new ArrayList<>();
		
		
		fos.write(FILE_TAG_DB.getBytes());
		fos.write(endl);
		for(Database db : databases.values()){
			fos.write(db.serialize());
			fos.write(endl);
			tables.addAll(db.getTables());
		}
		
		fos.write(FILE_TAG_TABLE.getBytes());
		fos.write(endl);
		for(Table tb : tables){
			fos.write(tb.serialize());
			fos.write(endl);
			groups.addAll(tb.getGroups());
			fields.addAll(tb.getFields().values());
			entries.addAll(tb.getEntries());
		}
		
		fos.write(FILE_TAG_FIELD.getBytes());
		fos.write(endl);
		for(Field f : fields){
			fos.write(f.serialize());
			fos.write(endl);
		}
		
		fos.write(FILE_TAG_GROUP.getBytes());
		fos.write(endl);
		ArrayList<byte[]> columndata = new ArrayList<>();
		int gcId = 0;
		for(Group g : groups){
			fos.write(g.serialize());
			fos.write(endl);
			for(Field f: g.getFields()){
				NumericValue vId = new NumericValue(++gcId);
				columndata.add(DBValue.valuesAsData(vId, g.getTable().getDatabase().getId(), g.getTable().getId(), g.getID(), f.getId()));
			}
		}
		
		fos.write(FILE_TAG_GROUP_COLUMN.getBytes());
		fos.write(endl);
		for(byte[] d : columndata){
			fos.write(d);
			fos.write(endl);
		}
		
		fos.write(FILE_TAG_ENTRY.getBytes());
		fos.write(endl);
		for(Entry e : entries){
			fos.write(e.serialize());
			fos.write(endl);
		}
		
		fos.close();
	}

	public Database getDatabase(NumericValue databaseId) throws NoSuchDatabaseException {
		return getDatabase(databaseId.getValue());
	}
	
	public Database getDatabase(int dbId) throws NoSuchDatabaseException {
		Database db = databases.get(dbId);
		if(db == null)throw new NoSuchDatabaseException(dbId);
		return db;
	}
	
	public boolean dropDatabase(int dbId){
		Database db = databases.get(dbId);
		if(db == null)return false;
		databasesIndexName.remove(db.getName());
		databases.remove(dbId);
		return true;
	}
	
//	public Table createTable(String databaseName, String tableName) throws NoSuchDatabaseException, IllegalNameException{
//		if(databaseName == null || tableName == null){
//			throw IllegalNameException.nameCannotBeNull();
//		}
//		
//		Database db = databases.get(databaseName);
//		if(db == null){
//			throw new NoSuchDatabaseException(databaseName);
//		}
//		else {
//			return db.createTable(tableName);
//		}		
//	}
}
